package ads1.ss14.can;


import java.util.HashMap;
import java.util.Map;
import ads1.ss14.can.exceptions.CANException;
import ads1.ss14.can.exceptions.NoAdditionalStorageAvailable;
import ads1.ss14.can.exceptions.NoSuchDocument;


public class Client implements ClientInterface, ClientCommandInterface {

  private final String id;
  private final int networkXSize;
  private final int networkYSize;

  private Area area;
  private int maxNumberOfDocuments;
  private Map<String, ClientInterface> neighborsMap;
  private Map<String, Document> documentsMap;

  /**
   * Constructs a new Client
   * 
   * @param uniqueID the ID of the Client in the CAN
   * @param networkXSize the size along the horizontal axis in the CAN
   * @param networkYSize the size along the vertical axis in the CAN
   */
  public Client(String uniqueID, int networkXSize, int networkYSize) {
    this.id = uniqueID;
    this.networkXSize = networkXSize;
    this.networkYSize = networkYSize;

    // will be set by calling the respective setter methods
    area = null;
    maxNumberOfDocuments = -1;
    neighborsMap = new HashMap<String, ClientInterface>();
    documentsMap = new HashMap<String, Document>();
  }

  @Override
  public String getUniqueID() {
    return id;
  }

  @Override
  public void setMaxNumberOfDocuments(int m) {
    this.maxNumberOfDocuments = m;
  }

  @Override
  public int getMaxNumberOfDocuments() {
    return maxNumberOfDocuments;
  }

  @Override
  public Document getDocument(String documentName) throws NoSuchDocument {
    if (documentsMap.containsKey(documentName)) {
      return documentsMap.get(documentName);
    } else {
      throw new NoSuchDocument();
    }
  }

  @Override
  public void storeDocument(Document d, Position p) throws NoAdditionalStorageAvailable, CANException {
    if (documentsMap.size() >= maxNumberOfDocuments) {
      throw new NoAdditionalStorageAvailable();
    }

    if (documentsMap.containsKey(d.getName())) {
      throw new CANException("A document with name " + d.getName() + " is already stored");
    }

    documentsMap.put(d.getName(), d);
  }

  @Override
  public void deleteDocument(String documentName) throws NoSuchDocument {
    if (!documentsMap.containsKey(documentName)) {
      throw new NoSuchDocument();
    }

    documentsMap.remove(documentName);
  }

  @Override
  public Position getPosition() {
    if (area == null) {
      return null;
    }

    double x = area.getLowerX() + ((area.getUpperX() - area.getLowerX()) / 2);
    double y = area.getLowerY() + ((area.getUpperY() - area.getLowerY()) / 2);

    return new Position(x, y);
  }

  @Override
  public Area getArea() {
    return area;
  }

  @Override
  public void setArea(Area newArea) {
    this.area = newArea;
  }

  @Override
  public Iterable<ClientInterface> getNeighbours() {
    return neighborsMap.values();
  }

  @Override
  public void addNeighbour(ClientInterface newNeighbour) {
    neighborsMap.put(newNeighbour.getUniqueID(), newNeighbour);
  }

  @Override
  public void removeNeighbour(String clientID) {
    neighborsMap.remove(clientID);
  }

  @Override
  public ClientInterface searchForResponsibleClient(Position p) {
    if (area.contains(p)) {
      return this;
    }

    ClientInterface minNeighbor = null;
    double minDist = Double.MAX_VALUE;

    for (ClientInterface neighbor : getNeighbours()) {
      if (neighbor.getArea().contains(p)) {
        return neighbor;
      }

      double dist = calcEuclideanDistance(neighbor.getArea(), p);

      if (minNeighbor == null || dist < minDist) {
        minNeighbor = neighbor;
        minDist = dist;
      } else if (dist == minDist) {
        if (minNeighbor.getUniqueID().compareTo(neighbor.getUniqueID()) < 0) {
          minNeighbor = neighbor;
        }
      }
    }

    if (minNeighbor != null) {
      return minNeighbor.searchForResponsibleClient(p);
    } else {
      return null;
    }
  }

  @Override
  public Iterable<Pair<Document, Position>> removeUnmanagedDocuments() {
    // NOT IMPLEMENTED
    return null;
  }

  @Override
  public void adaptNeighbours(ClientInterface joiningClient) {
    // NOT IMPLEMETED
  }

  /***************************************************************************************/
  /** COMMAND INTERFACE **/

  @Override
  public ClientInterface joinNetwork(ClientInterface entryPoint, Position p) throws CANException {
    // NOT IMPLEMENTED
    return null;
  }

  @Override
  public void addDocumentToNetwork(Document d) throws CANException {
    for (int i = 0; i < (networkXSize * networkYSize); i++) {
      double x = hashX(d.getName(), i);
      double y = hashY(d.getName(), i);
      Position p = new Position(x, y);

      ClientInterface c = searchForResponsibleClient(p);

      assert c != null;

      try {
        c.storeDocument(d, p);
        return;
      } catch (NoAdditionalStorageAvailable e) {
        // the client is full, rehash and try again with another client
      }
    }
  }

  @Override
  public void removeDocumentFromNetwork(String documentName) {
    for (int i = 0; i < (networkXSize * networkYSize); i++) {
      double x = hashX(documentName, i);
      double y = hashY(documentName, i);
      Position p = new Position(x, y);

      ClientInterface c = searchForResponsibleClient(p);

      assert c != null;

      try {
        c.deleteDocument(documentName);
        return;
      } catch (NoSuchDocument e) {
        // Document not found, continue to search
      }
    }
  }

  @Override
  public Document searchForDocument(String documentName) throws CANException {

    for (int i = 0; i < (networkXSize * networkYSize); i++) {
      double x = hashX(documentName, i);
      double y = hashY(documentName, i);
      Position p = new Position(x, y);

      ClientInterface c = searchForResponsibleClient(p);

      assert c != null;

      try {
        return c.getDocument(documentName);
      } catch (NoSuchDocument e) {
        // Document not found, continue to search
      }
    }

    return null;
  }

  /****************************************************************************************/

  private double hashX(String documentName, int i) {
    int M = networkXSize * networkYSize;
    int S = ordSum(documentName);

    return (hash(M, S, i) % networkXSize);
  }

  private double hashY(String documentName, int i) {
    int M = networkXSize * networkYSize;
    int S = ordSum(documentName);

    return Math.floor(hash(M, S, i) / networkXSize);
  }

  private boolean isLeftNeighbor(ClientInterface c) {
    return c.getArea().getUpperX() <= this.getArea().getLowerX();
  }

  private boolean isRightNeighbor(ClientInterface c) {
    return c.getArea().getLowerX() >= this.getArea().getUpperX();
  }

  private boolean isTopNeighbor(ClientInterface c) {
    return c.getArea().getLowerY() >= this.getArea().getUpperY();
  }

  private boolean isBottomNeighbor(ClientInterface c) {
    return c.getArea().getUpperY() <= this.getArea().getLowerY();
  }

  /****************************************************************************************/

  private static double calcEuclideanDistance(Area area, Position p) {
    double rx = Math.max(Math.min(area.getUpperX(), p.getX()), area.getLowerX());
    double ry = Math.max(Math.min(area.getUpperY(), p.getY()), area.getLowerY());

    return Math.sqrt((Math.pow((rx - p.getX()), 2) + Math.pow((ry - p.getY()), 2)));
  }

  private static int ord(char c) {
    return Character.getNumericValue(c) - Character.getNumericValue('a');
  }

  private static int ordSum(String s) {
    int sum = 0;
    for (int i = 0; i < s.length(); i++) {
      sum += ord(s.charAt(i));
    }

    return sum;
  }

  private static int rh(int M, int S, int i) {
    int j = S % (M - 2);
    return ((2 * j) + 1) * i;
  }

  private static int hash(int M, int S, int i) {
    int r = rh(M, S, i);
    return ((S % M) + r) % M;
  }
}
